/* Copyright 2009
 *
 * This program and the accompanying materials
 * are made available under the terms of the
 * Eclipse Public License v1.0 which accompanies
 * this distribution, and is available at
 *
 * 		http://www.eclipse.org/legal/epl-v10.html
 *
 * Unless required by applicable law or agreed to in
 * writing, software distributed under the License is
 * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES
 * OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing
 * permissions and limitations under the License.
 *
 * Contributors:
 * 	   IBM Corporation - initial API and implementation for JDT/DLTK
 *     Sean W. Quinn - initial adoption for use with PHP from various sources.
 */
package org.eclipse.php.internal.ui.corext.util;

import java.util.Map;

import org.eclipse.core.resources.IProject;
import org.eclipse.php.core.ToolFactory;
import org.eclipse.php.core.formatter.PHPCodeFormatter;
import org.eclipse.php.core.formatter.PHPCodeFormatterConstants;
import org.eclipse.php.internal.core.PHPCorePlugin;
import org.eclipse.php.internal.core.ast.nodes.ASTNode;
import org.eclipse.php.internal.core.ast.nodes.BodyDeclaration;
import org.eclipse.php.internal.core.ast.nodes.Expression;
import org.eclipse.php.internal.core.ast.nodes.Statement;
import org.eclipse.php.internal.formatter.PHPFormatterConstants;
import org.eclipse.text.edits.TextEdit;

/**
 * @author Sean W. Quinn (swquinn@gmail.com)
 */
public class CodeFormatterUtil {

	/**
	 * Creates edits that describe how to format the given string. Returns
	 * <code>null</code> if the code could not be formatted for the given kind.
	 *
	 * @param kind Use to specify the kind of the code snippet to format. It can
	 *            be any of the kind constants defined in
	 *            {@link PHPCodeFormatter}
	 * @param source The source to format
	 * @param offset The given offset to start recording the edits (inclusive).
	 * @param length the given length to stop recording the edits (exclusive).
	 * @param indentationLevel The initial indentation level, used to shift
	 *            left/right the entire source fragment. An initial indentation
	 *            level of zero or below has no effect.
	 * @param lineSeparator The line separator to use in formatted source, if
	 *            set to <code>null</code>, then the platform default one will
	 *            be used.
	 * @param options The options map to use for formatting with the default
	 *            code formatter. Recognized options are documented on
	 *            <code>JavaCore#getDefaultOptions()</code>. If set to
	 *            <code>null</code>, then use the current settings from
	 *            <code>JavaCore#getOptions</code>.
	 * @return an TextEdit describing the changes required to format source
	 * @throws IllegalArgumentException If the offset and length are not inside
	 *             the string, a IllegalArgumentException is thrown.
	 */
	public static TextEdit format(int kind, String source, int offset, int length, int indentationLevel,
			String lineSeparator, Map options) {
		if (offset < 0 || length < 0 || offset + length > source.length()) {
			throw new IllegalArgumentException(
					"offset or length outside of string. offset: " + offset + ", length: " + length + ", string size: " + source.length()); //$NON-NLS-1$//$NON-NLS-2$//$NON-NLS-3$
		}
		return ToolFactory.createPHPCodeFormatter(options).format(kind, source, offset, length, indentationLevel,
				lineSeparator);
	}

	/**
	 * Creates edits that describe how to format the given string. Returns
	 * <code>null</code> if the code could not be formatted for the given kind.
	 *
	 * @param kind Use to specify the kind of the code snippet to format. It can
	 *            be any of the kind constants defined in
	 *            {@link PHPCodeFormatter}
	 * @param source The source to format
	 * @param indentationLevel The initial indentation level, used to shift
	 *            left/right the entire source fragment. An initial indentation
	 *            level of zero or below has no effect.
	 * @param lineSeparator The line separator to use in formatted source, if
	 *            set to <code>null</code>, then the platform default one will
	 *            be used.
	 * @param options The options map to use for formatting with the default
	 *            code formatter. Recognized options are documented on
	 *            <code>JavaCore#getDefaultOptions()</code>. If set to
	 *            <code>null</code>, then use the current settings from
	 *            <code>JavaCore#getOptions</code>.
	 * @return an TextEdit describing the changes required to format source
	 * @throws IllegalArgumentException If the offset and length are not inside
	 *             the string, a IllegalArgumentException is thrown.
	 */
	public static TextEdit format(int kind, String source, int indentationLevel, String lineSeparator, Map options) {
		return format(kind, source, 0, source.length(), indentationLevel, lineSeparator, options);
	}

	/**
	 * Creates edits that describe how to format the given string. The given
	 * node is used to infer the kind to use to format the string. Consider to
	 * use {@link #format(int, String, int, String, Map)} if the kind is already
	 * known. Returns <code>null</code> if the code could not be formatted for
	 * the given kind.
	 *
	 * @param node Use to infer the kind of the code snippet to format.
	 * @param source The source to format
	 * @param indentationLevel The initial indentation level, used to shift
	 *            left/right the entire source fragment. An initial indentation
	 *            level of zero or below has no effect.
	 * @param lineSeparator The line separator to use in formatted source, if
	 *            set to <code>null</code>, then the platform default one will
	 *            be used.
	 * @param options The options map to use for formatting with the default
	 *            code formatter. Recognized options are documented on
	 *            <code>JavaCore#getDefaultOptions()</code>. If set to
	 *            <code>null</code>, then use the current settings from
	 *            <code>JavaCore#getOptions</code>.
	 * @return an TextEdit describing the changes required to format source
	 * @throws IllegalArgumentException If the offset and length are not inside
	 *             the string, a IllegalArgumentException is thrown.
	 */
	public static TextEdit format(ASTNode node, String source, int indentationLevel, String lineSeparator, Map options) {
		int code;
		String prefix = ""; //$NON-NLS-1$
		String suffix = ""; //$NON-NLS-1$
		if (node instanceof Statement) {
			code = PHPCodeFormatter.K_STATEMENTS;
			if (node.getType() == ASTNode.SWITCH_CASE) {
				prefix = "switch(1) {"; //$NON-NLS-1$
				suffix = "}"; //$NON-NLS-1$
				code = PHPCodeFormatter.K_STATEMENTS;
			}
		}
		else if (node instanceof Expression && node.getType() != ASTNode.VARIABLE) {
			code = PHPCodeFormatter.K_EXPRESSION;
		}
		else if (node instanceof BodyDeclaration) {
			code = PHPCodeFormatter.K_CLASS_BODY_DECLARATIONS;
		}
		else {
			switch (node.getType()) {
			case ASTNode.ARRAY_ELEMENT:
			case ASTNode.VARIABLE:
			case ASTNode.SCALAR:
				suffix = " x;"; //$NON-NLS-1$
				code = PHPCodeFormatter.K_CLASS_BODY_DECLARATIONS;
				break;
			case ASTNode.FIELD_DECLARATION:
			case ASTNode.SINGLE_FIELD_DECLARATION:
				suffix = ";"; //$NON-NLS-1$
				code = PHPCodeFormatter.K_STATEMENTS;
				break;
			case ASTNode.NAMESPACE_NAME:
			case ASTNode.USE_STATEMENT:
			case ASTNode.USE_STATEMENT_PART:
				suffix = "\nclass A {}"; //$NON-NLS-1$
				code = PHPCodeFormatter.K_COMPILATION_UNIT;
				break;
			case ASTNode.COMMENT:
				suffix = "void foo();"; //$NON-NLS-1$
				code = PHPCodeFormatter.K_CLASS_BODY_DECLARATIONS;
				break;
			case ASTNode.CATCH_CLAUSE:
				prefix = "try {}"; //$NON-NLS-1$
				code = PHPCodeFormatter.K_STATEMENTS;
				break;
			case ASTNode.FIELD_ACCESS:
			case ASTNode.METHOD_INVOCATION:
			case ASTNode.FUNCTION_INVOCATION:
				// Javadoc formatting not yet supported:
				return null;
			default:
				//Assert.isTrue(false, "Node type not covered: " + node.getClass().getName()); //$NON-NLS-1$
				return null;
			}
		}

		String concatStr = prefix + source + suffix;
		TextEdit edit = format(code, concatStr, prefix.length(), source.length(), indentationLevel, lineSeparator,
				options);
		if (edit != null && prefix.length() > 0) {
			edit.moveTree(-prefix.length());
		}
		return edit;
	}

	/**
	 * Creates edits that describe how to re-format the given string. This
	 * method should be used for formatting existing code. Returns
	 * <code>null</code> if the code could not be formatted for the given kind.
	 *
	 * @param kind Use to specify the kind of the code snippet to format. It can
	 *            be any of the kind constants defined in
	 *            {@link PHPCodeFormatter}
	 * @param source The source to format
	 * @param offset The given offset to start recording the edits (inclusive).
	 * @param length the given length to stop recording the edits (exclusive).
	 * @param indentationLevel The initial indentation level, used to shift
	 *            left/right the entire source fragment. An initial indentation
	 *            level of zero or below has no effect.
	 * @param lineSeparator The line separator to use in formatted source, if
	 *            set to <code>null</code>, then the platform default one will
	 *            be used.
	 * @param options The options map to use for formatting with the default
	 *            code formatter. Recognized options are documented on
	 *            <code>JavaCore#getDefaultOptions()</code>. If set to
	 *            <code>null</code>, then use the current settings from
	 *            <code>JavaCore#getOptions</code>.
	 * @return an TextEdit describing the changes required to format source
	 * @throws IllegalArgumentException If the offset and length are not inside
	 *             the string, a IllegalArgumentException is thrown.
	 */
	public static TextEdit reformat(int kind, String source, int offset, int length, int indentationLevel,
			String lineSeparator, Map options) {
		if (offset < 0 || length < 0 || offset + length > source.length()) {
			throw new IllegalArgumentException(
					"offset or length outside of string. offset: " + offset + ", length: " + length + ", string size: " + source.length()); //$NON-NLS-1$//$NON-NLS-2$//$NON-NLS-3$
		}
		return ToolFactory.createPHPCodeFormatter(options, ToolFactory.M_FORMAT_EXISTING).format(kind, source, offset,
				length, indentationLevel, lineSeparator);
	}

	/**
	 * Returns the possibly <code>project</code>-specific core preference
	 * defined under <code>key</code>.
	 *
	 * @param project the project to get the preference from, or
	 *            <code>null</code> to get the global preference
	 * @param key the key of the preference
	 * @return the value of the preference
	 * @since 3.1
	 */
	private static String getCoreOption(IProject project, String key) {
		if (project == null) {
			return PHPCorePlugin.getOption(key);
		}
		return PHPFormatterConstants.TAB;
	}

	/**
	 * Returns the possibly <code>project</code>-specific core preference
	 * defined under <code>key</code>, or <code>def</code> if the value is not a
	 * integer.
	 *
	 * @param project the project to get the preference from, or
	 *            <code>null</code> to get the global preference
	 * @param key the key of the preference
	 * @param def the default value
	 * @return the value of the preference
	 * @since 3.1
	 */
	private static int getCoreOption(IProject project, String key, final int defaultValue) {
		try {
			return Integer.parseInt(getCoreOption(project, key));
		}
		catch (NumberFormatException e) {
			return defaultValue;
		}
	}

	/**
	 * Gets the current tab width.
	 *
	 * @param project The project where the source is used, used for project
	 *            specific options or <code>null</code> if the project is
	 *            unknown and the workspace default should be used
	 * @return The tab width
	 */
	public static int getTabWidth(IProject project) {
		/*
		 * If the tab-char is SPACE, FORMATTER_INDENTATION_SIZE is not used by
		 * the core formatter. We piggy back the visual tab length setting in
		 * that preference in that case.
		 */
		String key;
		if (PHPFormatterConstants.SPACE.equals(getCoreOption(project, PHPCodeFormatterConstants.FORMATTER_TAB_CHAR))) {
			key = PHPCodeFormatterConstants.FORMATTER_INDENTATION_SIZE;
		}
		else {
			key = PHPCodeFormatterConstants.FORMATTER_TAB_SIZE;
		}

		return getCoreOption(project, key, 4);
	}
}
